package com.qp.opc;


import com.qp.common.FnKit;
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfig;
import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider;
import org.eclipse.milo.opcua.sdk.client.api.identity.IdentityProvider;
import org.eclipse.milo.opcua.sdk.client.api.identity.UsernameProvider;
import org.eclipse.milo.opcua.stack.client.DiscoveryClient;
import org.eclipse.milo.opcua.stack.core.Identifiers;
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
import org.eclipse.milo.opcua.stack.core.types.builtin.*;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UByte;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
import org.eclipse.milo.opcua.stack.core.types.enumerated.BrowseDirection;
import org.eclipse.milo.opcua.stack.core.types.enumerated.BrowseResultMask;
import org.eclipse.milo.opcua.stack.core.types.enumerated.NodeClass;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.eclipse.milo.opcua.stack.core.types.structured.BrowseDescription;
import org.eclipse.milo.opcua.stack.core.types.structured.BrowseResult;
import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription;
import org.eclipse.milo.opcua.stack.core.types.structured.ReferenceDescription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.ExecutionException;

/**
 * @author TARZAN
 */
public class OpcUaUtil {
    //日志
    private static final Logger logger = LoggerFactory.getLogger(OpcUaUtil.class);


    /**
     * 方法描述: 创建客户端
     *
     * @param inConfig 配置
     * @return {@link OpcUaClient}
     */
    public static OpcUaClient createClient(OpcUaConfig inConfig) throws Exception {
        Path securityTempDir = Paths.get(System.getProperty("java.io.tmpdir"), "security");
        Files.createDirectories(securityTempDir);
        if (!Files.exists(securityTempDir)) {
            throw new Exception("unable to create security dir: " + securityTempDir);
        }

        OpcUaKeyStoreLoader loader = new OpcUaKeyStoreLoader().load(securityTempDir);
        //获取安全策略
        List<EndpointDescription> endpointDescription = DiscoveryClient.getEndpoints(inConfig.getUrl()).get();
        //过滤出一个自己需要的安全策略
        EndpointDescription endpoint = endpointDescription.stream()
                .filter(e -> e.getSecurityPolicyUri().equals(SecurityPolicy.None.getUri()))
                .findFirst().orElse(null);
        IdentityProvider identityProvider = new AnonymousProvider();
        if (!FnKit.isEmpty(inConfig.getUsername()) || !FnKit.isEmpty(inConfig.getPassword())) {
            identityProvider = new UsernameProvider(inConfig.getUsername(), inConfig.getPassword());
        }
        // 设置配置信息
        OpcUaClientConfig config = OpcUaClientConfig.builder()
                // opc ua 自定义的名称
                .setApplicationName(LocalizedText.english("T1"))
                // 地址
                .setApplicationUri("urn:eclipse:milo:examples:inConfig.getClient()")
                .setCertificate(loader.getClientCertificate())
                .setKeyPair(loader.getClientKeyPair())
                // 安全策略等配置
                .setEndpoint(endpoint)
                .setIdentityProvider(identityProvider)
                //等待时间
                .setRequestTimeout(UInteger.valueOf(5000))
                .build();
        // 准备连接
        OpcUaClient opcClient = OpcUaClient.create(config);
        //开启连接
        opcClient.connect().get();
        logger.info("连接成功。。。success");
        return opcClient;

    }

    /**
     * 方法描述: 查询所有节点
     *
     * @param inClient 代理客户端
     * @return 所有的属性
     */
    public static Set<String> getAllKeys(OpcUaClientFacade inClient) throws Exception {
        if (inClient.getClient() == null) {
            throw new RuntimeException("连接客户端不能为空");
        }
        return browse(inClient, null);
    }


    /**
     * 方法描述: 查询某个节点下的所有节点
     *
     * @param inClient 代理客户端
     * @return 所有的子节点
     */
    public static Set<String> browse(OpcUaClientFacade inClient, String identifier) throws Exception {
        if (inClient.getClient() == null) {
            throw new RuntimeException("连接客户端不能为空");
        }
        Set<String> keys = new HashSet<>(500);
        browse(inClient, identifier, keys);
        return keys;
    }

    /**
     * 方法描述: 查询某个节点下的所有节点
     *
     * @param inClient   代理客户端
     * @param identifier 父属性标识
     * @param keys       属性集合
     * @return 属性标识
     */
    private static Set<String> browse(OpcUaClientFacade inClient, String identifier, Set<String> keys) throws Exception {
        if (inClient.getClient() == null) {
            throw new RuntimeException("连接客户端不能为空");
        }
        NodeId nodeId = Identifiers.ObjectsFolder;
        if (!FnKit.isEmpty(identifier)) {
            nodeId = new NodeId(inClient.getNamespaceIndex(), identifier);
        }
        BrowseDescription browse = new BrowseDescription(
                nodeId,
                BrowseDirection.Forward,
                Identifiers.References,
                true,
                UInteger.valueOf(NodeClass.Object.getValue() | NodeClass.Variable.getValue()),
                UInteger.valueOf(BrowseResultMask.All.getValue())
        );
        BrowseResult browseResult = inClient.getClient().browse(browse).get();
        ReferenceDescription[] references = browseResult.getReferences();
        String key;
        for (ReferenceDescription reference : references) {
            key = reference.getNodeId().getIdentifier().toString();
            if (key.contains("._Hints")
                    || key.contains("._System")
                    || key.contains("._InternalTags")
                    || key.contains("_Statistics")) {
                continue;
            }
//            logger.info(reference.getNodeId().getIdentifier().toString());
            keys.add(reference.getNodeId().getIdentifier().toString());
            if (reference.getNodeClass().getValue() == NodeClass.Object.getValue()) {
                browse(inClient, reference.getNodeId().getIdentifier().toString(), keys);
            }
        }
        return keys;
    }


    /**
     * 方法描述: 读取单个节点值
     *
     * @param inClient   代理客户端
     * @param identifier 属性标识
     * @return 值
     */
    public static Object readValue(OpcUaClientFacade inClient, String identifier) throws Exception {
        if (inClient.getClient() == null) {
            throw new RuntimeException("连接客户端不能为空");
        }
        NodeId nodeId = new NodeId(inClient.getNamespaceIndex(), identifier);
        DataValue value = null;

        value = inClient.getClient().readValue(0.0, TimestampsToReturn.Both, nodeId).get();

        if (Objects.nonNull(value) && Objects.nonNull(value.getValue()) && Objects.nonNull(value.getValue().getValue())) {
            return value.getValue().getValue();
        }
        return null;
    }

    /**
     * @param inClient 代理客户端 命名空间
     * @param keys     点位集合
     * @return 值
     */
    public static List<DataValue> readValues(OpcUaClientFacade inClient, List<String> keys) throws Exception {
        if (inClient.getClient() == null) {
            throw new RuntimeException("连接客户端不能为空");
        }
        List<NodeId> nodeIdList = new ArrayList<>(500);
        keys.forEach(e -> {
            NodeId nodeId = new NodeId(inClient.getNamespaceIndex(), e);
            nodeIdList.add(nodeId);
        });
        List<DataValue> dataValues = inClient.getClient().readValues(0.0, TimestampsToReturn.Both, nodeIdList).get();
        return dataValues;
    }

    /**
     * 读取值到Map中
     *
     * @param inClient 代理客户端
     * @param keys     点位集合
     * @return 值
     */
    public static Map<String, Object> readValuesToMap(OpcUaClientFacade inClient, List<String> keys) throws Exception {
        if (inClient.getClient() == null) {
            throw new RuntimeException("连接客户端不能为空");
        }
        List<NodeId> nodeIdList = new ArrayList<>(500);
        keys.forEach(e -> {
            NodeId nodeId = new NodeId(inClient.getNamespaceIndex(), e);
            nodeIdList.add(nodeId);
        });
        List<DataValue> dataValues;
        try {
            dataValues = readValues(inClient, keys);
        } catch (Exception ex) {
            String msg = ex.getMessage();
            if (msg == null) {
                msg = "";
            }
            if (msg.indexOf("Bad_Timeout") >= 0) {
                throw new Exception("OPC读取keyList：" + FnKit.join(keys) + "连接超时，返回空值");
            } else {
                throw ex;
            }
        }
        Map<String, Object> dataMap = new HashMap<>();
        if (dataValues == null || dataValues.size() == 0) {
            return dataMap;
        }

        String key;
        DataValue dv;
        for (int i = 0; i < keys.size(); i++) {
            key = keys.get(i);
            dv = dataValues.get(i);
            if (Objects.nonNull(dv) && Objects.nonNull(dv.getValue()) && Objects.nonNull(dv.getValue().getValue())) {
                dataMap.put(key, dv.getValue().getValue());
            }
        }
        return dataMap;
    }

    /**
     * 写入值
     *
     * @param inClient   代理客户端
     * @param identifier 属性标识
     * @param value      值
     */
    public static void writeValue(OpcUaClientFacade inClient, String identifier, Object value) throws Exception {
        if (inClient.getClient() == null) {
            throw new RuntimeException("连接客户端不能为空");
        }
        if(value instanceof Short) {
            writeValueShortFinal(inClient,identifier,value);
        } else  if(value instanceof Boolean) {
            writeBooleanValue(inClient,identifier,(Boolean) value);
        } else  if(value instanceof Byte) {
            writeByteValue(inClient,identifier,(Byte)value);
        } else {
            writeObjectValue(inClient,identifier,value);
        }
    }


    /**
     * 写入值
     *
     * @param inClient   代理客户端
     * @param identifier 属性标识
     * @param value      值
     */
    public static void writeObjectValue(OpcUaClientFacade inClient, String identifier, Object value) throws Exception {
        if (inClient.getClient() == null) {
            throw new RuntimeException("连接客户端不能为空");
        }
        //创建变量节点
        NodeId nodeId = new NodeId(inClient.getNamespaceIndex(), identifier);
        //创建Variant对象和DataValue对象
        Variant v = new Variant(value);
        DataValue dataValue = new DataValue(v, null, null);
        StatusCode statusCode = inClient.getClient().writeValue(nodeId, dataValue).get();
        if (statusCode.isGood()) {
            logger.info("******写入成功  " + identifier + "," + value);
        } else {
            logger.error("******写入失败  " + identifier + "," + value);
        }
    }

    /**
     * 写入Byte值
     *
     * @param inClient   代理客户端
     * @param identifier 属性标识
     * @param value      值
     */
    public static void writeByteValue(OpcUaClientFacade inClient, String identifier, byte value) throws Exception {
        if (inClient.getClient() == null) {
            throw new RuntimeException("连接客户端不能为空");
        }
        //创建变量节点
        NodeId nodeId = new NodeId(inClient.getNamespaceIndex(), identifier);
        //创建Variant对象和DataValue对象
        Variant v = new Variant(UByte.valueOf(value));
        DataValue dataValue = new DataValue(v, null, null);
        StatusCode statusCode = inClient.getClient().writeValue(nodeId, dataValue).get();
        if (statusCode.isGood()) {
            logger.info("******写入成功  " + identifier + "," + value);
        } else {
            logger.error("******写入失败  " + identifier + "," + value);
        }
    }

    /**
     * 写入布尔值
     *
     * @param inClient   代理客户端
     * @param identifier 属性标识
     * @param value      值
     */
    public static void writeBooleanValue(OpcUaClientFacade inClient, String identifier, boolean value) throws Exception {
        if (inClient.getClient() == null) {
            throw new RuntimeException("连接客户端不能为空");
        }
        //创建变量节点
        NodeId nodeId = new NodeId(inClient.getNamespaceIndex(), identifier);
        //创建Variant对象和DataValue对象
        Variant v = new Variant(value);
        DataValue dataValue = new DataValue(v, null, null);
        StatusCode statusCode = inClient.getClient().writeValue(nodeId, dataValue).get();
        if (statusCode.isGood()) {
            logger.info("******写入成功  " + identifier + "," + value);
        } else {
            logger.error("******写入失败  " + identifier + "," + value);
        }
    }

    /**
     * 写入数值
     *
     * @param inClient   代理客户端
     * @param identifier 属性标识
     * @param value      值
     */
    public static void writeValueShortFinal(OpcUaClientFacade inClient, String identifier, Object value) throws Exception {
        if (inClient.getClient() == null) {
            throw new RuntimeException("连接客户端不能为空");
        }
        int loopTimes = 0;
        writeObjectValue(inClient, identifier, value);
        Object tempValue;
        while (loopTimes < 3) {
            tempValue = readValue(inClient, identifier);
            if (tempValue != null && value.toString().equals(tempValue.toString())) {
                break;
            }
            writeObjectValue(inClient, identifier, value);
            loopTimes++;
        }
    }

    /**
     * 方法描述:  写入多个节点的值
     *
     * @param inClient 代理客户端 命名空间
     * @param keys     节点集合
     * @param values   值集合
     * @return {@link Object}
     */
    public static Object writeValues(OpcUaClientFacade inClient, Set<String> keys, List<Object> values) throws Exception {
        if (inClient.getClient() == null) {
            throw new RuntimeException("连接客户端不能为空");
        }
        List<NodeId> nodeIs = new ArrayList<>(keys.size());
        keys.forEach(e -> {
            NodeId nodeId = new NodeId(inClient.getNamespaceIndex(), e);
            nodeIs.add(nodeId);
        });
        List<DataValue> dataValues = new ArrayList<>(values.size());
        values.forEach(e -> {
            Variant value = new Variant(Double.parseDouble(e.toString()));
            DataValue dataValue = new DataValue(value);
            dataValues.add(dataValue);
        });
        inClient.getClient().writeValues(nodeIs, dataValues).get();
        return null;
    }


    /**
     * 方法描述: 断开连接
     *
     * @param client 代理客户端
     */
    public static void disconnect(OpcUaClient client) {
        if (client == null) {
            return;
        }
        try {
            client.disconnect().get();
        } catch (InterruptedException | ExecutionException e) {
            logger.error(e.getMessage(), e);
        }
    }

}


